/*
   (C) 2005 - Michael Ocean - Boston University

  This work is a part of the larger snBench project.  Information
  can be found at: http://csr.bu.edu/snbench

  This program is free software; you can redistribute it and/or
  modify it under the terms of Version 2 of the GNU General Public 
  License.
  
  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  General Public License for more details.
  
  You should have received a copy of the GNU Library General Public
  License along with this library; if not, write to the Free
  Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

package sxe;

import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.io.*;

import step.FunctionWrapper;

public class SxeClassLoader extends ClassLoader {

	/* Ensure the function with the given opcode is available on this XE.  If not,
	 * get it.
	 */

	private SxeClassLoader(){		
	}
	static private SxeClassLoader scl = null;
	
	public static FunctionWrapper ResolveOpcode(String opcode) throws ClassNotFoundException{
		String jarfile = null;
		/*
		 * DOME : we need a way for the requestor to give 
		 * us the jar url explicitly, as in the case where a
		 * exp node includes it:
		 *   <exp opcode="..." jar="...">
		 */

		if(scl==null)
			scl = new SxeClassLoader();
		Class c = null;

		if(opcode==null){ /* DOME : can this ever happen?  should it?
				    Document it or assert(opcode!=null) */
			System.err.println("SxeClassLoader: Opcode is null!?!?!");
			return null;
		}
		if(opcode.startsWith("sxe.core.")){
			/* 
			 * this is core functionality that all SXEs have.  Simply load
			 * the class and return.
			 *
			 * DOME : if a particular jar is specified, skip this.
			 */	
			c = null;

			try {
				c = Class.forName(opcode);
			} catch (Exception ex) {
				System.err.println("forName load failed: " + ex.getMessage());
			}

			if (c == null)
				c = scl.loadClass(opcode);
		} else {
			/* Opcode is not loaded.  Must retrieve from the server */

			// jarfile = SxeMonitor.requestJarFromRM(opcode); // DOME !

			if(jarfile!=null){
				try{
					c = scl.loadJar( jarfile, opcode );
				}
				catch(Exception e){
					throw new ClassNotFoundException(opcode);
				}
			}
		}
		if(c==null){
			System.err.println("SxeClassLoader failed to find '" + opcode + "'");
			throw new ClassNotFoundException(opcode);
		}
		
		System.out.println(c.toString());
		try{
		FunctionWrapper fw = (FunctionWrapper)c.newInstance();
		return fw;
		}
		catch(Exception e){
			e.printStackTrace(System.out);
			throw new ClassCastException(e.toString());
		}
		
	}	
	
	public Class loadJar(String name, String classname) throws Exception{
		/* mjo : nope.  This code won't be invoked unless the VM
		 * is missing the class in it's cache.
		 * adb : it looks to me like this could lead to lots of
		 * spurious repeated loads of the same frequently-used 
		 * jar files.  Shouldn't we rather maintain a table of
		 * already-loaded jars (periodically flushed, like any
		 * good cache) so we don't need to hit the network each time?
		 */
		File f = new File(name);
		if(!f.exists())
			throw new FileNotFoundException("Could not find specified JAR file:" + name);
	
		URLClassLoader loader = new URLClassLoader(new URL[] { f.toURL() });
    		    
	    Class c = loader.loadClass (classname);

	    return c;
	}
	
    public Class findClass(String name) throws ClassNotFoundException {
    	/* In Java 5, this method is called by loadClass as such:
    	 *    1. If a class has already been loaded, it returns it.
    	 *    2. Otherwise, it delegates the search for the new class to the parent class loader.
    	 *    3. If the parent class loader doesn't find the class, loadClass() calls the method findClass() to find and load the class.
    	 * 
    	 * i.e., this is the last chance to load the class for the user
    	 */
    	Class clas = null;

    	//	 Create a pathname from the class name
    	//	 E.g. java.lang.Object => java/lang/Object
    	String fileStub = name.replace( '.', '/' );
    	String javaFilename = fileStub+".java";
    	String classFilename = fileStub+".class";
    	File javaFile = new File( javaFilename );
    	File classFile = new File( classFilename );

      	// if no existing class exists
    	if (!classFile.exists() ) {
    		try {
				if (!javaFile.exists() ) {
					throw new ClassNotFoundException( "No source exists for: "+javaFilename );
				}    			
    				if (!compile( javaFilename ) ) {
    					throw new ClassNotFoundException( "Compile failed: "+javaFilename );
    				}
    		} catch( IOException ie ) {
    			throw new ClassNotFoundException( ie.toString() );
    		}
    	}
    	// load the bytes (either just compiled, or previously existing)
    	try {
    		byte raw[] = getBytes( classFilename );

    		clas = defineClass( name, raw, 0, raw.length );
    	} catch( IOException ie ) {
	    	/*	 This might not have been a failure in JDK prior to 1.4,
	    	 * (e.g., this might still be a library object) but this is
	    	 * no longer the case as loadClass would not have called this
	    	 * method in such a case.
	    	 */
			throw new ClassNotFoundException( name );
    	}
    	//	System.out.println( "findSystemClass: "+clas );
    	//	 Resolve the class, if any, but only if the "resolve"
    	//	 flag is set to true
    	if (clas != null)
    		resolveClass( clas );
    	//	 If we still don't have a class, it's an error
		if (clas == null)
			throw new ClassNotFoundException( name );

		return clas;

    }

    private boolean checkName(String name, boolean allowArrayClass) {
        if ((name == null) || (name.length() == 0))
            return true;
        if (name.indexOf('/') != -1)
            return false;
        if (!allowArrayClass && (name.charAt(0) == '['))
            return false;
        return true;
      }

    /* Given a filename, read the entirety of that file from disk
     *  and return it as a byte array.
     */
	private byte[] getBytes( String filename ) throws IOException {
		//	 Find out the length of the file
		File file = new File( filename );
		long len = file.length();
		//	 Create an array that's just the right size for the file's
		//	 contents
		byte raw[] = new byte[(int)len];
		//	 Open the file
		FileInputStream fin = new FileInputStream( file );
		//	 Read all of it into the array; if we don't get all,
		//	 then it's an error.
		int r = fin.read( raw );
		if (r != len)
			throw new IOException( "Can't read all, "+r+" != "+len );
		//	 Don't forget to close the file!
		fin.close();
		//	 And finally return the file contents as an array
		return raw;
		}
	
	/* Spawn a process to compile the java source code file
	 *  specified in the 'javaFile' parameter. Return a true if
	 *  the compilation worked, false otherwise.
	 */
	private boolean compile( String javaFile ) throws IOException {
		//	 Let the user know what's going on
		System.out.println( "SCL: Compiling "+javaFile+"..." );
		//	 Start up the compiler
		Process p = Runtime.getRuntime().exec( "javac "+javaFile );
		
		//	 Wait for it to finish running
		try {
			p.waitFor();
		} catch( InterruptedException ie ) { System.out.println( ie ); }
		
		//	 Check the return code, in case of a compilation error
		int ret = p.exitValue();
		//	 Tell whether the compilation worked
		return ret==0;
		}
	

	public static void main(String[] args) {
		System.out.println(getOpcodeLibraryAsXML());
	}
	
	/* gets an XML formatted list of the opcodes supported by this SXE */
	public static String getOpcodeLibraryAsXML(){
			
		List<String> opcodes = getOpcodeLibrary();
	    StringBuffer sb = new StringBuffer("<opcodeLibrary>\n");
	    //return classes;
	    for(String s: opcodes){ 
			sb.append("\t<opcode>" + s + "</opcode>\n");
		}
	    sb.append("</opcodeLibrary>\n");
	    
	    return sb.toString();
	}

	/* gets an list of the opcodes supported by this SXE */
	public static List<String> getOpcodeLibrary(){
			
		List<String> classes = new ArrayList<String>();
		List<String> opcodes = new ArrayList<String>();

	    try {
	    	System.out.println("searching for snBench opcodes...");
	        // get the system classpath
	        String classpath = System.getProperty("java.class.path", "");

	        if (classpath.equals("")) {
	            System.err.println("error: classpath is not set");
	        }

	        System.out.println("system classpath = " + classpath);
	            
	        StringTokenizer st =
	            new StringTokenizer(classpath, File.pathSeparator);
	        
	        while (st.hasMoreTokens()) {
	            String token = st.nextToken();
	            File classpathElement = new File(token);
	            
                List<String> listFromThisToken =
                    (classpathElement.isDirectory() ?
                     locateClassesFromDir(classpathElement) :
                     locateClassesFromJar(classpathElement));
                
                classes.addAll(listFromThisToken);

	        }
	    	System.out.println("search for snBench opcodes complete");
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
		
	    SxeClassLoader scl = new SxeClassLoader();
	    
	    //return classes;    
	    for(String s: classes){ 
	    	if(s.startsWith("sxe."))
	    	try{
	    		s = s.substring(0,s.lastIndexOf("."));
	    		Class c = scl.loadClass(s);
	    		
	    		FunctionWrapper fw = (FunctionWrapper)c.newInstance();
	    		    		
	    		fw = null;
	    		c = null;
	    		opcodes.add(s);
	    	}catch(Exception e){
				// not an opcode don't add it to the list	
				// System.out.println("'" + s + "' is not an opcode");
			}

	    }
	    
	    return opcodes;
	}
	
	private static List<String>  locateClassesFromJar(File jarFile) {
		List<String> files = new ArrayList<String>();
		try {
			if(jarFile.getName().endsWith(".jar")){
				System.out.println(jarFile+" is being scanned");
				Enumeration<JarEntry> fileNames;
					fileNames = new JarFile(jarFile).entries();
				JarEntry entry = null;
				while(fileNames.hasMoreElements()){
					entry = fileNames.nextElement();
					if(entry.getName().endsWith(".class")){
						files.add(entry.getName());
					}
				}
			}
		} catch (IOException e) {	
			e.printStackTrace();
		}
	
		return files;
	}
	
	   private static List<String> locateClassesFromDir(File classpathElement) {
	        List<String> allEntries = locateClassesFromDir(classpathElement, null);
	        return allEntries;
	    }

	    private static List<String> locateClassesFromDir(File classpathElement, String prefix) {
	        List<String> allEntries = new ArrayList<String>();  //this will contain all entries in this directory
	        //these are all the class files directly in this directory
	        List<String> classEntries = Arrays.asList(classpathElement.list(new CLASSFilter()));  
	        //add the prefix to all entries
	        if(prefix != null){
	            for (int i = 0, c = classEntries.size(); i < c; i++) {
	                allEntries.add(prefix + "." + classEntries.get(i));
	            }
	        }
	        else {
	            for (int i = 0, c = classEntries.size(); i < c; i++) {
	              allEntries.add(classEntries.get(i));
	           }
	        }
	        
	        //now get a list of all directories within the current directory
	        List<File> allSubdirectories = Arrays.asList(classpathElement.listFiles(new DirectoryFilter()));
	        //loop through all of them and load their classes, adding this directory to the prefix list.
	        for(int i = 0, c = allSubdirectories.size(); i < c; i++){
	            allEntries.addAll(locateClassesFromDir(allSubdirectories.get(i), (prefix == null ? "" : prefix + "." ) + allSubdirectories.get(i).getName()));
	        }
	        return allEntries;
	    }

	    static class DirectoryFilter implements FileFilter {
	        public boolean accept(File dir) {
	            return dir.isDirectory();
	        }
	    }
	    static class CLASSFilter implements FilenameFilter{
	    	public boolean accept(File dir, String name) {
	    		return (name.endsWith(".class"));
	    	}
	    }
}
